大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux

Redux是什么

在介绍 redux 之前,我们先来了解下目前前端最火的框架之一 — react

react 官网上,我们可以看到其介绍是

用于构建用户界面的 JavaScript 库

也就是说 react 本质上是一个 JavaScript 的库,是创建UI接口的视图层框架

(图一)

image

如图一所示,假如蓝色组件需要和灰色组件通信,只使用 react 视图层框架,就需要调用父组件函数的形式通信,逐层往父级通信

但对于大型应用来说,这样实现基本不太可能,过多的组件会造成维护困难,那应该怎么做呢?

这个时候就应该在 react 视图层框架上配套一个数据层框架 — Redux ,结合应用

redux 要求我们把数据都放在 store 公共存储空间,当绿色组件想要去传递数据时,只需要改变 store 里边对应的数据,灰色区域会自动感知到 store 有变化,就会重新去 store 取数据,从而灰色组件就能得到新的数据

这样的操作流程对于深层次的组件是非常适用的,组件与组件之间的数据传递会变得非常简单

组件改变,修改数据,其他组件再来取值。这就是 Redux 的基础设计理念

Redux = Reducer + Flux

在讲解这个式子之前,我们先来看看 redux 的起源

react 在2013年开源的时候, facebook 团队除了放出 react 框架外,还放出了 flux 框架,是官方推出的最原始的辅助 react 使用的数据层框架

后来在使用时发现了 flux 的很多缺点,比如公共数据存储区域可以有很多个 store 组成,这样数据存储操作就可能存在一个数据依赖的问题

接着就有人对 flux 做了一个升级,也就是现在使用的 redux , redux 除了借鉴 flux 的设计理念外,还引入了一个新的概念 — Reducer

(图二)

image

图二就是 redux 的工作流程图,再次说明了其设计理念就是把所有数据放在 store 进行管理,一个组件改变了 store 里的数据内容,其他组件就能感知到 store 的变化,再来取数据,从而间接的实现了这些数据传递的功能

Redux的工作流程

根据图二的工作流程,可以举个实际的例子:假设 React Components 是借书的用户, Action Creactor 是借书时说的话(借什么书), Store 是图书馆管理员, Reducer 是记录本(借什么书,还什么书,在哪儿,需要查一下), state 是书籍信息

整个流程就是借书的用户需要先存在,然后需要借书,需要一句话来描述借什么书,图书馆管理员听到后需要查一下记录本,了解图书的位置,最后图书馆管理员会把这本书给到这个借书人

转换为代码是, React Components 需要获取一些数据, 然后它就告知 Store 需要获取数据,这就是就是 Action Creactor , Store 接收到之后去 Reducer 查一下, Reducer 会告诉 Store 应该给这个组件什么数据

Store的创建

我们已经了解到 redux 是解决数据传递问题的框架,把所有的数据都放在store中进行管理。所以 store 数据仓库是四个操作过程中最重要的,应该最先被创建

接下来我们用 redux 来编写简易的 TodoList , 通过具体的例子进行说明

假设你已经通过 create-react-app 创建了一个 react 项目, 这里首先直接引用 antd 进行布局,然后还得引入 redux

npm install redux --save

npm install antd --save

创建 Store 文件夹, 接着在文件夹中创建 index.js

import { createStore } from 'redux' // 引入一个第三方的方法

const store = createStore() // 创建数据的公共存储区域(管理员)

export default store

这样就已经把 redux 引入到项目中了,以及创建了一个 store 的公共数据区域,还需要一个记录本去辅助管理数据,也就是 reducer

继续创建一个 reducer.js ,这个文件需要返回一个函数,接收两个参数: state , action ;需要返回一个值,默认返回 state , state 可以理解为整个数据空间里存放的数据,可以设置一个默认值

const defaultState = {}

export default (state = defaultState, action) => {
  return state
}

现在记录本有了,那最后怎么把笔记本传给 store 呢?继续修改 index.js 的代码

import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)

export default store

这样就将 reducer 和 store建立了连接

我们已经创建了一个 store ,负责存储项目应用中的所有数据, 一个 reducer ,负责整个项目应用中的数据处理,并且把 reducer 传给了 store ,这样就可以知道在 reducer 中查看数据并做处理了

在创建 TodoList 组件时,有两项需要获取的数, inputValuelist ,可以在 defaultState 中对其设置初始值,由 reducer 来管理,由于 reducer 会传入到 store ,所以 store 也就知道数据空间里存在 inputValue 和 list 数据

const defaultState = {
  inputValue: '123',
  list: [1, 2]
}

组件的数据应该怎样从公用的数据空间获取呢?我们需要在 TodoList 组件中引入这个 store

import store from './store'

class TodoList extends Component {
  
  constructor(props) {
    super(props)
    console.log(store.getState())
  }

  render () {
    ...
  }
}

很明显,这个值能传到组件中,接着就可以在组件中进行页面的数据渲染了

render () {
  return (
    <div style={{marginTop: '10px', marginLeft: '10px'}}>
      <div>
        <Input value={this.state.inputValue} placeholder='todo info' style={{width: '300px', marginRight: '10px'}}></Input>
        <Button type="primary">提交</Button>
      </div>
      <List
        style={{marginTop: '10px', width: '300px'}}
        bordered
        dataSource={this.state.list}
        renderItem={item => (<List.Item>{item}</List.Item>)}
      />
    </div>
  )
}

简单概括 store 的创建就是:

  1. 需要引入 redux 方法,叫做 createStore

  2. 不能单单的创建 store ,需要在创建 store 的时候把 reducer 传递进来

那 reducer 里边存放的内容呢?

  1. reducer负责管理整个业务里边的数据,包括处理数据,存储数据等

  2. reducer 返回的必须是一个函数,这个函数里面接收两个参数,一个是 state ,另一个是 action

看到这里,我相信有的小伙伴会有疑问, store 里边应该存什么数据呢?我们在 state = defaultState 这里设置了这个仓库的默认数据是什么? action 有什么用呢?接着继续看下面的操作

Action 和 Reducer 的编写

上面已经完成 react 组件取数据这样一个过程,接下来我们继续改进:当 input 里的内容发生改变时, redux 里的数据 value 也可以相应发生变化

可以再次回到图二,应该先创建一个 action , action 是个对象的形式,里边需要有个 type 向 redux 描述需要做的操作, 然后把 value 值传递进去

组件创建完 action 后,接着就需要把这个 action 派发给 store

constructor(props) {
  super(props)
  this.state = store.getState()
  this.handleInputChange = this.handleInputChange.bind(this)
}

... ...

handleInputChange(e) {
  const action = {
    type: 'change_input_value',
    value: e.target.value
  }
  store.dispatch(action)
}

但是 store 并不知道怎么处理这个数据,需要去 reducer 进行查找,所以需要把当前 store 里存在的数据和接收的 action 转给 reducer , reducer 处理好了之后再转给 store

需要注意的是, react 里边的 store ,接收到 action 之后,会自动把之前的数据和 action 转发给 reducer

export default (state = defaultState, action) => {
  console.log(state, action)
  return state
}

state 是上一次 store 中的数据集合,action 是 dispatch 传过来的对象

reducer 已经接收到了数据,也拿到了 action ,接着就需要对这些数据进行处理,最后传给 store

export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state)) // 对之前的state做一次深拷贝
    newState.inputValue = action.value
    return newState
  }
  return state
}

注意, reducer 有一个限制, reducer 可以接收 state ,但是绝不能修改 state 。这就是为什么拿到 state 的时候需要去拷贝一份,再对拷贝的数据进行修改了

最后 reducer 返回的 newState 给了谁呢?从图二中我们可以发现, reducer 将处理的新数据传给了 store , store用新数据替换成老数据,那页面是怎么进行更新的呢?

这就需要用到 store 的另一个方法 - store.subscribe() ,意思是组件订阅了store,store里的数据只要发生改变,subscribe() 里边的函数就会执行

但是 subscribe() 里边的函数应该怎样写,才能让 store 一改变,页面就跟着变化呢?

constructor(props) {
  super(props)
  this.state = store.getState()
  this.handleInputChange = this.handleInputChange.bind(this)
  this.handleStoreChange = this.handleStoreChange.bind(this)
  store.subscribe(this.handleStoreChange) 
}

handleStoreChange() {
  this.setState(store.getState())
}

也就是说,当组件感知到 store 里的数据发生变化时,就去调用 store.getState() 方法,从 store 里重新取数据,然后调用 setState 方法,替换掉当前组件里的数据,这样组件里的数据就和 store 里边的数据同步了

接下来继续增加提交功能,当提交发生的时候, input 里的值需要存入公共数据里的 list,同样的逻辑

  1. 需要先给 button 绑定一个事件

  2. 创建一个 action (对象),指定一个类型,然后通过 dispatch 把 action 发给 store

  3. 然后 store 把之前 store 里的数据和 action 发给 reducer , reducer 这个函数接收到 state 和 action 之后会对数据做一些处理,会返回一个新的 state到 store

  4. 最后 store 会将新的 state 替换以前 store 的数据, react 组件会感知到 store 数据发生了变化,会从 store 里边重新取数据,更新组件的内容,页面就发生了变化

(TodoList.js)

<Button type="primary" onClick={this.handleBtnClick}>提交</Button>

handleBtnClick() {
  const action = {
    type: 'add_todo_item',
  }
  store.dispatch(action)
}

(reducer.js)

export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.inputValue = action.value
    return newState
  }
  if (action.type === 'add_todo_item') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.list.push(newState.inputValue)
    newState.inputValue = ''
    return newState
  }
  return state
}

使用 redux 完成 todolist 删除功能

先在每个 item 上设置点击事件

 <List
  style={{marginTop: '10px', width: '300px'}}
  bordered
  dataSource={this.state.list}
  renderItem={(item, index) => (<List.Item onClick={this.handleItemDelete.bind(this, index)}>{item}</List.Item>)}
/>

接下来需要改变 store 里的数据,怎么改变呢?同样的,需要先创建一个 action ,然后传给 store

handleItemDelete(index) {
  const action = {
    type: 'delete_todo_item',
    index
  }
  store.dispatch(action)
}

store 接收到这个 action 之后,就会把之前的数据和这个 action 一起传给 reducer 进行处理

if (action.type === 'delete_todo_item') {
  const newState = JSON.parse(JSON.stringify(state))
  newState.list.splice(action.index, 1) // 找到对应的下标,删除即可
  return newState
}

actionTypes的拆分

如果经常操作 action ,可能会发现一个问题, action 的 type 这个字符串要是有一个字符写错了,程序就垮掉了,而且很难被排查出,那应该怎么避免这个问题呢?

可以在 store 文件夹中新建一个文件 actionTypes.js ,然后设置每个 action 字符串的常量,用这些常量分别替换掉 action 的 type 字符串

export const CHANGE_INPUT_VALUE = 'change_input_value'
export const ADD_TODO_ITEM = 'add_todo_item'
export const DELETE_TODO_ITEM = 'delete_todo_item'

这样抽离的目的是因为如果常量或者变量写错的时候,是能报出详细异常的,可以迅速定位到问题

使用 actionCreator 统一创建 action

回到图二 redux 的工作流程图,在派发 action 的时候, action 不应该在我们的组件里直接被定义,一般会通过 actionCreator 来统一的管理页面上所有的 action ,然后通过 actionCreator 来创建 action ,这是一个比较标准、正规的流程。怎么做呢?

在 store 文件夹下创建 actionCreator.js 的文件时,就可以创建一些方法

import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'

export const getInputChangeAction = (value) => ({
  type: CHANGE_INPUT_VALUE,
  value
})

然后在 TodoList.js 组件中引入这个文件,更新方法

handleInputChange(e) {
  const action = getInputChangeAction(e.target.value)
  store.dispatch(action)
}

之所以将 action 的创建放在 actionCreator 这样一个统一的文件进行管理,主要的目的是提高代码的可维护性,而且前端会有自动化的测试工具,如果把 action 都放在一个文件里边,做测试的时候也会非常方便

现在回到图二 redux 的流程图,是不是就非常清晰了~~~

如果要改变 store 里的数据,就要先去调用 actionCreator ,创建一个 action ,然后 store 把这个 action 派发出去,这样流程就完全一致了

组件精炼

  1. 实际项目中,最好将 UI 组件和容器组件拆分, UI 组件负责页面渲染,容器组件负责页面逻辑

  2. 当组件中只有一个 render 函数时,就可以定义成无状态组件

比如 TodoListUI 组件

class TodoListUI extends Component {
  render() {
    return (
      <div style={{marginTop: '10px', marginLeft: '10px'}}>
        // ... ...
      </div>
    )
  }
}

就可以修改成无状态组件

const TodoListUI = (props) => {
  return (
    <div style={{marginTop: '10px', marginLeft: '10px'}}>
      // ... ...
    </div>
  )
}

无状态组件的性能比较高,因为它就是一个函数,而 React 里边普通的组件是 JS 里边的一个类,这个类生成的对象里,还会有一些生命周期函数,所以它执行起来,既要执行生命周期函数,又要执行 render ,它要执行的东西远比函数执行的东西多的多,所以一个普通组件的性能是肯定赶不上无状态组件的

小结

Redux 设计和使用的三项原则

  1. 首先 store 要求必须是唯一的

  2. 只有 store 能够改变自己的内容

有的小伙伴可能会疑惑,明明是 reducer 对数据进行了整理,其实 reducer 只是将原有数据和新的 action 进行了整理,最终还是需要把新的 state 返回给 store , store 拿到 reducer 的数据,再对自己的数据进行更新

  1. reducer 必须是纯函数

纯函数指的是,给定固定的输入,就一定会有固定的输出,而且不会有任何副作用,如果一个函数里边有 ajax 等异步操作,或者与日期相关的操作之后,他都不是一个纯函数,副作用是指对传入的参数进行修改

Redux 中核心的 API

  1. createStore 可以帮助创建 store

  2. store.dispatch 帮助派发 action , action 会传递给 store

  3. store.getState 这个方法可以帮助获取 store 里边所有的数据内容

  4. store.subscrible 方法可以让让我们订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行

使用 Redux-thunk 中间件进行ajax请求发送

在讲解 Redux-thunk 这个中间件之前,我们先写一个在 react 中直接获取异步数据的例子

React中发送异步请求获取数据

先进行模拟数据的测试,假设你已经安装了 Charles ,打开 Charles ,然后找到 Tools 下面的 Mac Local ,选中,进行如图操作

image

image

image

当然,之前还得新建一个 list.json 文件,如

["hello", "dell", "lee"]

然后刷新页面,就可以看到打印出来请求的json数据了

image

接下来,就可以进行创建 action , store 派发 action 的操作了

首先在 actionCreator.js 里边创建一条 action 对象

export const initListAction = (data) => ({
  type: INIT_LIST_ACTION,
  data
})

然后在 actionTypes 中申明这个 action type 常量

export const INIT_LIST_ACTION = 'init_list_action'

接着在 TodoList 组件中实例化生成 action,并 dispatch action 到 store , store 再连同之前 store 的数据一同派发给 reducer

componentDidMount() {
  axios.get('/list.json').then((res) => {
    const data = res.data
    const action = initListAction(data)
    store.dispatch(action)
  })
}

reducer 对数据进行整理,最后将整理好的数据返回给 store

if (action.type === INIT_LIST_ACTION) {
  const newState = JSON.parse(JSON.stringify(state))
  newState.list = action.data
  return newState
}

使用 Redux-thunk 中间件进行ajax请求发送

上面的 TodoList 组件代码, list 在 componentDidMount 做了一个ajax数据的请求,咋一看可能没有什么问题,但是,如果我们把这种异步的请求,或者把一些非常复杂的逻辑都放在组件里进行实现时,这个组件会显得过于臃肿

所以遇到这种异步请求或者非常复杂的逻辑,最好是把它移出到其他页面进行统一的处理,可以移到哪里进行管理呢?

这个时候 Redux-thunk 这个中间件就显得至关重要了,它可以将这些异步请求或者是复杂的逻辑放到 action 去处理,那如何使用 Redux-thunk 这个中间件呢?

打开github,搜索 Redux-thunk ,star最多的项目,就是Redux-thunk

按照它的使用说明进行如下操作

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk) // applyMiddleware可以使用中间件模块
) 

export default store

需要注意的是:

  1. 中间件是通过创建 redux 的 store 时使用的,所以这个中间件是指的 redux 中间件,而不是 react 中间件

  2. 原则上 action 返回的是一个对象,但当我们使用 redux-thunk 中间件后, action 就可以返回一个函数了,继而可以在函数里边进行异步操作,也就可以把 TodoList 获取数据的请求放入这个函数中了

接着操作,在 actionCreator 中创建 action 的函数,然后数据传给 store

那问题来了,怎么传呢?本质还是调用 dipatch 方法,但是现在 actionCreactor 这个文件里并没有 store 这个数据仓库,也就没有 dispatch 这个方法,怎么办呢?

实际上,当我们创建一个内容是函数的 action 时,返回的函数就会自动接收到 store.dispatch 这个方法,所以只要在返回的函数里调用 dispatch ,然后派发 action 就好了, store 判断接收的 action 是一个对象,就会接收并发送给 reducer 进行数据更新操作

export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/list.json').then((res) => {
      const data = res.data
      const action = initListAction(data)
      dispatch(action)
    })
  }
}

在 TodoList 组件中引用这个创建内容是函数的 action

componentDidMount() {
  const action = getTodoList()
  store.dispatch(action) // 调用 store.dispatch()这个函数时,action这个函数就会被执行
}

有的小伙伴可能会有疑问,就一个ajax请求,放在 componentDidMount 会有影响吗?

考虑到后期代码量的增加,如果把异步函数放在组件的生命周期里,这个生命周期函数会变得越来越复杂,组件就会变得越来越大

所以,还是应该把这种复杂的业务逻辑或者异步函数拆分到一个地方进行管理,现在借助 redux-thunk ,就可以放在 actionCreactor 里边集中管理,除此之外,在做自动化测试的时候,测试 actionCreactor 这个方法,也会比测组件的生命周期函数要简单的多

到底什么是 Redux 中间件

(图三)

image

看到图三,我们先来回顾一个redux的标准流程:

view 到 redux 的过程中会派发一个 action , action 通过 Store 的 dispatch 方法,会派发给 store , store接收到 action ,再连同之前的 state 一起传给 reducer , reducer 返回一个新的数据给 store , store 就可以去改变自己的 state ,组件接收到新的 state 就可以重新渲染页面了

redux的中间件在这个流程里边,指的是谁和谁之间呢?指的是 action 和 store 中间

继续看图三,action 通过 dispatch 方法被传递给 store ,那么 action 和 store 之间是不是就是 dispatch 这个方法呢?实际上,我们说的中间件就是指的 dispatch 方法的一个封装,或者是对 dispatch 方法的一个升级

最原始的 dispatch 方法,接收到对象 action 后会传递给 store ,这就是没有中间件的情况

对 dispatch 方法做了一个升级后,也就是使用中间件时,再调用 dispatch 方法,如何给 dispatch 传递的仍然是个对象, dispatch 就会把这个对象传给 store ,跟之前的方法没有任何区别;但是假如传的是个函数,就不会直接传递给 store 了,会让这个函数先执行,然后执行完之后需要调用 store ,这个函数再去调用 store

dispatch方法会根据参数的不同,执行不同的事情,如果参数是对象,就直接传给store,如果是函数,那就把函数执行结束

所以,redux的中间件原理很简单,就是对 store 的 dispatch 方法做一个升级,既可以接收对象,又可以接收函数了,那是用什么方法进行的升级的呢?就是用 redux-thunk 这个中间件进行升级的

当然,redux的中间件还有 redux-log ,原理就是在派发 action 给 store 之前先 console.log 出来;还有 redux-saga ,接下来需要讲解的

Redux-saga 中间件的使用

redux-saga 也是做异步代码拆分的,可以完全替代 redux-thunk

在 github 中搜索 redux-saga ,翻到文档部分,根据文档进行如下操作

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import createSagaMiddleware from 'redux-saga'

const sagaMiddleware = createSagaMiddleware() // 创建saga中间件

// 创建数据的公共存储区域
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
) 

export default store

这里还需要在store中建一个单独的文件- saga.js

function* mySaga() {
  
}

export default mySaga

mySaga() 是 ES6 的 generator 函数

没有使用 redux-saga 时, action 只能给到 store , store 再把之前的数据和 action 给到 reducer ,所以我们只能在 reducer 里拿到 store 去做一些业务逻辑

需要注意的是,有了redux-saga之后, saga.js 也可以接收这个 action了

import { takeEvery } from 'redux-saga/effects'
import { GET_INIT_LIST } from './actionTypes'
import { initListAction } from './actionCreator'
import axios from 'axios'

function* getInitList() {
  axios.get('/list.json').then((res) => {
    const data = res.data
    const action = initListAction(data)
    console.log(action)
  })
}

// generator 函数
function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList) // takeEvery捕捉每一个派发出来的action type类型为GET_INIT_LIST的时候,就会执行getInitList方法
}

export default mySaga

上面的代码是什么意思呢?首先,当 TodoList 这个容器组件加载完成后,会派发一个 action ,因为之前在创建 store 时使用了 redux-saga 这个中间件,做了基础的配置,所以这个 action 派发出来之后,不仅仅 reducer 会接收到这个 action , saga 文件中 mySaga 这个函数也能接收到,刚好通过 takeEvery 这个函数声明,一旦接收到 GET_INIT_LIST 这样类型的 action ,就执行 getInitList 这个方法,所以就可以把异步逻辑写到这个方法里了

通过在异步函数中创建 action ,还需要把它派发出去,但是在 saga.js 这个文件中并没有 store 数据仓库,所以不能执行 store.dispatch(action) 这个操作,接下来我们会用到另一个方法 - put

继续看 github 上 redux-saga 的例子

在 generator 函数里边我们可以不用 promise 来请求异步数据,可以这么来写

import { takeEvery, put  } from 'redux-saga/effects'
import { GET_INIT_LIST } from './actionTypes'
import { initListAction } from './actionCreator'
import axios from 'axios'

function* getInitList() {
  const res = yield axios.get('/list.json')
  const action = initListAction(res.data)
  yield put(action)
}

function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList)
}

export default mySaga

整个执行流程就是:

  1. 首先在创建 store 的时候,根据官方文档的配置,需要把 redux-saga 的使用配置做好,这里需要注重的是:

    在引入 createSagaMiddleware 后,需要创建一个 createSagaMiddleware ,然后通过 applyMiddleware 使用这个中间件,接着创建 saga.js 这个文件,然后在 store 的 index 中引入这个文件,让这个文件通过 sagaMiddleware 来运行

  2. saga 里边要有一个 generator 函数,在这个 generator 函数里边写入一些逻辑,意思是当接收到 action 的类型是 GET_INIT_LIST 时,就执行 getInitList 方法,这个方法是一个 generator 函数,接着就可以在 getInitList 方法里进行数据的获取发送操作了

当我们获取ajax数据失败的时候,为了操作友好,最后做下容错处理

function* getInitList() {
  try {
    const res = yield axios.get('/list.json')
    const action = initListAction(res.data)
    yield put(action)
  } catch(e) {
    console.log('list.json 网络请求失败')
  }
}

通过上面的实践可以发现, redux-saga 远比 redux-thunk 复杂的多, redux-saga 里边有非常多的api,我们只用了 takeEveryput ,文档中还有很多我们经常用到的 calltakeLatest

在处理大型项目时, redux-saga 是要优于 redux-thunk 的;但是从另一角度来说, redux-thunk 几乎没有任何 api ,特点就是在 action 里面返回的内容不仅仅是个对象,还可以是个函数

React-Redux 的使用

目前我们已经了解了 react 和 redux ,那 React-Redux 是什么呢?它是一个第三方的模块,可以在 react 中非常方便是使用 redux

重新来编写 todolist 功能,在 index 文件中引入 react-redux

import React from 'react'
import ReactDOM from 'react-dom'
import TodoList from './TodoList'
import { Provider } from 'react-redux'
import store from './store'

const App = (
  <Provider store={store}>
    <TodoList />
  </Provider>
)

ReactDOM.render(App, document.getElementById('root'))

Provider 实质是一个组件,是一个提供器,是 react-redux 的一个核心API,连接着 store , Provider 里边所有的组件,都有能力获取到 store 里边的内容

react-redux 的另一个核心方法叫做 connect ,接收三个参数,最后一个参数是连接的组件,前面两个是连接的规则

之前说 Provider 组件连接了 store , Provider 内部的组件有能力获取到 store ,是怎样获取的呢?就是通过 connect 这个方法获取到里面的数据的

意思是让 TodoList 组件和 store 进行连接,所以 connect 方法的意思是做连接,在做连接时需要有一定的方式和规则,就是用 mapStateToProps 方法来做关联,翻译为中文就是把 store 里的数据 inputValue 映射到组件 inputValue 这个位置,为组件的 props 的数据

import React, { Component } from 'react'
import { connect } from 'react-redux'

class TodoList extends Component {
  render () {
    return (
      <div>
        <div>
          <input value={this.props.inputValue} />
          <button>提交</button>
        </div>
        <ul>
          <li>Dell</li>
        </ul>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,

  }
}

export default connect(mapStateToProps, null)(TodoList)

如果需要对 store 的数据做修改,dispatch 是指的 store.dispatch ,可以通过 mapDispatchToProps 方法把 store.dispatch 挂载到props上,为什么呢?

因为想要改变 store 里的内容,就要调用 dispatch 方法, dispatch 方法被映射到了 props 上,所以就可以通过 this.props.dispatch 方法去调用了

import React, { Component } from 'react'
import { connect } from 'react-redux'

class TodoList extends Component {
  render () {
    return (
      <div>
        <div>
          <input value={this.props.inputValue} onChange={this.props.handleInputChange} />
          <button>提交</button>
        </div>
        <ul>
          <li>Dell</li>
        </ul>
      </div>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue
  }
}
const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList)

现在在 input 里输入值的功能就完成了,那todolist的增加功能怎么实现呢?

(TodoList.js)

<button onClick={this.props.handleClick}>提交</button>

const mapDispatchToProps = (dispatch) => {
  return {
    handleInputChange(e) {
      const action = {
        type: 'change_input_value',
        value: e.target.value
      }
      dispatch(action)
    },

    handleClick() {
      const action = {
        type: 'add_todo_item'
      }
      dispatch(action)
    }
  }
} 

(reducer.js)

export default (state = defaultState, action) => {
  if (action.type === 'change_input_value') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.inputValue = action.value
    return newState
  }
  if (action.type === 'add_todo_item') {
    const newState = JSON.parse(JSON.stringify(state))
    newState.list.push(newState.inputValue)
    newState.inputValue = ''
    return newState
  }
  return state
}

点击这个 button 的时候,会执行 handleClick 这个方法,这个方法会把创建出来的 action 传给 store ,再传给 reducer, reducer 接收到这个 action 之后,去处理数据,把新的数据返回出去,新的数据就包含列表项的新内容了,数据发生了改变,todolist 组件恰好又通过 connect 跟数据做了连接,所以这块是个自动的流程,数据一旦发生改变,这个组件自动就会跟的变

以前还需要 store.subscribe 做订阅,现在连订阅都可以不用了,页面自动跟随数据发生变化

这样写就实现了增加 item 的功能,后续还有一些功能的实现可以去我的 github 看完整代码

比如 item 的删除操作, action 要通过 actionCreator 来创建,同时,还需要把 action 的 type 字符串放在 actionType 里面进行管理等等

创建 TodoList 这个组件,正常来说都是 export default TodoList ,把这个组件导出出去,但是现在 export defalut 出的东西是通过 connect 方法执行的结果,connect 方法做了一件什么事呢?

它把这些映射关系和业务逻辑集成到了 TodoList 这个 UI 组件之中,所以 connect 方法可以这样理解,TodoList 是一个 UI 组件,当你用 connect 把这个 UI 组件和一些数据和逻辑相结合时,返回的内容实际就是一个容器组件了,容器组件可以理解成数据处理包括派发这样的业务逻辑,对 UI 组件进行包装,去调用这些UI组件,数据和方法都准备好了

有的小伙伴可能在网上看到过这样的描述,react-redux 组件既有 UI 组件,又有容器组件。UI 组件就是 TodoList 这个东西,而容器组件就是 connect 方法返回的结果,或者说 connect 方法执行生成的内容

所以 export default 导出的内容就是 connect 方法执行的结果,是一个容器组件

代码

附完整版代码地址:redux-todolist

内含

react-redux 完整的 todolist 代码

redux-saga 完整的 todolist 代码